package csci6450.chickenFriedFS.nuggetServer;

import csci6450.chickenFriedFS.nuggetCore.Nugget;
import csci6450.chickenFriedFS.nuggetCore.NuggetConstants;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Responsible for all operations on the Nugget File Store.
 * @author jacobschoen
 */
public class NuggetFileStoreManager {

    //the actual file that the nuggets are stored in
    private File fileStore;
    //list of all the slots and whether there is a nugget stored there or not
    private ArrayList<Slot> slots;
    private static NuggetFileStoreManager instance = new NuggetFileStoreManager();

    /**
     * Creates a new Instance of a Nugget File Store Manager. 
     */
    private NuggetFileStoreManager() {
        fileStore = new File(RuntimeSettings.getInstance().getFileStoreLocation());
        if (!fileStore.exists()) {
            try {
                System.out.println("Creating File Store.");
                this.createFileStore();
                System.out.println("Done Creating File Store.");
            } catch (FileNotFoundException ex) {
                Logger.getLogger(NuggetFileStoreManager.class.getName()).log(Level.SEVERE, null, ex);
            } catch (IOException ex) {
                Logger.getLogger(NuggetFileStoreManager.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        System.out.println("Filestore exists at " + fileStore.getAbsolutePath());

        this.format();
    }

    public static NuggetFileStoreManager getInstance() {
        return instance;
    }

    public void format() {
        System.out.println("Initializing " + RuntimeSettings.getInstance().getNumberOfChunks() + " slots.");
        this.slots = new ArrayList<Slot>();
        long offSet = 0;
        for (int i = 0; i < RuntimeSettings.getInstance().getNumberOfChunks(); i = i + 1) {
            this.slots.add(new Slot(offSet, true));
            offSet = offSet + NuggetConstants.CHUNK_SIZE;
        }
        Collections.sort(this.slots, SLOT_COMPARATOR);
        System.out.println("Done Initializing slots.");
    }

    /**
     * Returns the number of nuggets avalaible to be written to.
     * @return the number of nuggets available
     */
    public int getNumberOfAvalaibleNuggets() {
        int count = 0;
        for (int i = 0; i < slots.size(); i = i + 1) {
            if (slots.get(i).isFree()) {
                count = count + 1;
            }
        }
        return count;
    }

    /**
     * Returns the nugget starting at the offset passed in
     * @param offset
     * @return a byte[] containing the nugget data
     */
    public byte[] getNugget(long offset) throws FileNotFoundException, IOException {
        RandomAccessFile f = new RandomAccessFile(this.fileStore, "r");
        byte[] result = new byte[(int) NuggetConstants.CHUNK_SIZE];
        f.seek(offset);
        f.read(result);
        f.close();
        return result;
    }

    /**
     * Adds a nugget to the file system if there is available nuggets.
     * @param nugget the bytes to add to the file system
     * @return the byteOffSet to the nugget, or -1 if the system is full
     */
    public synchronized long addNugget(byte[] nugget) throws FileNotFoundException, IOException {
        long offSet = this.getNextOffset();
        if (offSet > 0) {
            RandomAccessFile f = new RandomAccessFile(this.fileStore, "rw");
            f.seek(offSet);
            f.write(nugget);
            f.close();
            return offSet;
        } else {
            return -1;
        }
    }

    protected synchronized void addNugget(Nugget nugget) {
        boolean done = false;
        for (int i = 0; i < this.slots.size(); i = i + 1) {
            if (nugget.getNuggetOffset() == this.slots.get(i).offSet) {
                this.slots.get(i).setFree(false);
                done = true;
                Collections.sort(this.slots, SLOT_COMPARATOR);
            }
        }
    }

    public long getNextOffset() {
        if (this.slots.get(0).isFree()) {
            long offSet = this.slots.get(0).getOffSet();
            this.slots.get(0).setFree(false);
            Collections.sort(this.slots, SLOT_COMPARATOR);
            return offSet;
        } else {
            return -1;
        }
    }

    /**
     * Removes a nugget from the file system.
     * @param offset the offset to the nugget to remove
     * @return true if successful, false otherwise
     */
    public boolean deleteNugget(long offset) {
        boolean found = false;
        for (int i = 0; i < this.slots.size(); i = i + 1) {
            if (this.slots.get(i).getOffSet() == offset) {
                this.slots.get(i).setFree(true);
                found = true;
            }
        }
        return found;
    }

    /**
     * This method is used to create a new empty File Store for the file system.
     * If there is already a File Store there it will be deleted and then create
     * a new empty File Store.
     */
    public void createFileStore() throws FileNotFoundException, IOException {
        if (fileStore.exists()) {
            fileStore.delete();
        }
        try {
            fileStore.getParentFile().mkdirs();
        } catch (Exception e) {
            e.printStackTrace();
        }
        fileStore.createNewFile();
        RandomAccessFile f = new RandomAccessFile(fileStore, "rw");
        f.setLength(RuntimeSettings.getInstance().getNumberOfChunks() * NuggetConstants.CHUNK_SIZE);
        f.close();
    }

    public void loadExistingNuggets(List<Nugget> nuggets) {
        for (int i = 0; i < nuggets.size(); i = i + 1) {
            this.slots.get(this.findSlotIndexByOffset(nuggets.get(i).getNuggetOffset())).setFree(false);
        }
        Collections.sort(this.slots);
    }

    private int findSlotIndexByOffset(long offset) {
        int result = -2;
        boolean done = false;
        for (int i = 0; !done && i < slots.size(); i = i + 1) {
            if (this.slots.get(i).getOffSet() == offset) {
                result = i;
                done = true;
            }
        }
        return result;
    }
    public static final Comparator SLOT_COMPARATOR =
            new Comparator() {

                public int compare(Object o1, Object o2) {
                    Slot s1 = ((Slot) o1);
                    Slot s2 = ((Slot) o2);
                    if ((s1.isFree() && s2.isFree()) || (!s1.isFree() && !s2.isFree())) {
                        return s1.getOffSet().compareTo(s2.getOffSet());
                    } else if (s1.isFree() && !s2.isFree()) {
                        return -1;
                    } else if (!s1.isFree() && s2.isFree()) {
                        return 1;
                    }
                    return -1;
                }
            };

    class Slot implements Comparable {

        private Long offSet;
        private boolean free;

        public Slot(Long offSet, boolean free) {
            this.offSet = offSet;
            this.free = free;
        }

        public Long getOffSet() {
            return this.offSet;
        }

        public boolean isFree() {
            return this.free;
        }

        public void setFree(boolean set) {
            this.free = set;
        }

        public int compareTo(Object o) {
            Slot s1 = this;
            Slot s2 = ((Slot) o);
            if ((s1.isFree() && s2.isFree()) || (!s1.isFree() && !s2.isFree())) {
                return s1.getOffSet().compareTo(s2.getOffSet());
            } else if (s1.isFree() && !s2.isFree()) {
                return -1;
            } else if (!s1.isFree() && s2.isFree()) {
                return 1;
            }
            return -1;
        }
    }
}
